﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;

namespace EOS.Framework.Web.Mvc
{    
    /// <summary>
    /// Helper class for writing xhtml to a stream. The class also writes the xml-namespace and the doctype.
    /// </summary>
    /// <remarks>
    /// Internal implementation, who also provides the flush method, which returns a output string. After the flush method
    /// is called not other methods can be called.
    /// </remarks>
    public class XhtmlWriter : IXhtmlWriter
    {
        private const string DefaultNamespaceUri = "http://www.w3.org/1999/xhtml";

        /// <summary>
        /// XmlWriter, for generating the xhtml document.
        /// </summary>
        private XmlWriter _writer;
        /// <summary>
        /// Stream for writing the output to.
        /// </summary>
        private MemoryStream _stream;
        /// <summary>
        /// True, if the output is flushed, otherwise false.
        /// </summary>
        private bool _flushed;
        /// <summary>
        /// Stack with all opened xhtml tags.
        /// </summary>
        private Stack<string> _elementStack;
        /// <summary>
        /// A list of all xhtml elements, who cannot have an inner xml.
        /// </summary>
        private static List<string> _emptyElements;

        /// <summary>
        /// Initializes the <see cref="XhtmlWriter"/> class.
        /// </summary>
        static XhtmlWriter()
        {
            _emptyElements = new List<string>(new string[] 
            {
                "area", "base", "basefont", "br", "col", 
                "frame", "hr", "img", "input", "isindex", 
                "link", "meta", "param"
            });
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="XhtmlWriter"/> class.
        /// </summary>
        public XhtmlWriter()
        {
            this._stream = new MemoryStream();
            this._elementStack = new Stack<string>();

            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = false;
            settings.NewLineChars = string.Empty;
            settings.Encoding = Encoding.UTF8;

            _writer = XmlTextWriter.Create(_stream, settings);
            _writer.WriteDocType("html", "-//W3C//DTD XHTML 1.0 Transitional//EN", "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd", null);
        }

        #region IXhtmlWriter Members

        /// <summary>
        /// Starts writing an xml element.
        /// </summary>
        /// <param name="localName">Name of the xml element. Cannot be null or empty or contains only blanks.</param>
        /// <remarks>
        /// Each started element must be ended to have a valid xml output.
        /// </remarks>
        /// <exception cref="InvalidOperationException">The started xhtml tag cannot have childs.</exception>
        public void WriteStartElement(string localName)
        {
            _elementStack.Push(localName);

            if (_flushed)
            {
                throw new InvalidOperationException(Properties.Resources.XhtmlAlreadyFlushed);
            }
            _writer.WriteStartElement(localName, DefaultNamespaceUri);
        }

        /// <summary>
        /// Closes one element and pops the corresponding namespace scope.
        /// </summary>
        /// <remarks>If the opened element is a element, which cannot be empty the method
        /// writes the full end tag.</remarks>
        public void WriteEndElement()
        {
            if (_flushed)
            {
                throw new InvalidOperationException(Properties.Resources.XhtmlAlreadyFlushed);
            }

            if (_elementStack.Count == 0)
            {
                throw new InvalidOperationException(Properties.Resources.XmlEndTagBeforeStartTag);
            }

            string localElement = _elementStack.Pop();
            if (_emptyElements.Contains(localElement.ToLower()))
            {
                _writer.WriteEndElement();
            }
            else
            {
                _writer.WriteFullEndElement();
            }
        }

        /// <summary>
        /// Writes out the attribute with the specified local name and value to the last opened xml tag.
        /// </summary>
        /// <param name="name">The local name of the attribute.</param>
        /// <param name="value">The value of the attribute.</param>
        public void WriteAttribute(string name, string value)
        {
            if (_flushed)
            {
                throw new InvalidOperationException(Properties.Resources.XhtmlAlreadyFlushed);
            }
            if (name.Contains(":") == false)
            {
                _writer.WriteAttributeString(name, value);
            }
        }

        /// <summary>
        /// Writes the given text content.
        /// </summary>
        /// <param name="text">Text to write.</param>
        /// <exception cref="InvalidOperationException">The started xml tag cannot have a value.</exception>
        public void WriteString(string text)
        {
            if (_flushed)
            {
                throw new InvalidOperationException(Properties.Resources.XhtmlAlreadyFlushed);
            }
            _writer.WriteString(text);
        }

        /// <summary>
        /// Writes raw markup manually from a string.
        /// </summary>
        /// <param name="data">String containing the text to write.</param>
        /// <remarks>This method does not escape special characters. When using this method, it cannot garantueed,
        /// that the class produces valid xhtml code.</remarks>
        public void WriteRaw(string data)
        {
            if (_flushed)
            {
                throw new InvalidOperationException(Properties.Resources.XhtmlAlreadyFlushed);
            }
            _writer.WriteRaw(data);
        }

        #endregion

        /// <summary>
        /// Generates the xml and returns it as a string.
        /// </summary>
        /// <returns>The generated xml.</returns>
        public string Flush()
        {
            if (_flushed)
            {
                throw new InvalidOperationException(Properties.Resources.FlushCalledTwice);
            }

            if (_elementStack.Count > 0)
            {
                string elementsOutput = string.Empty;
                for (int i = 0; i < _elementStack.Count; i++)
                {
                    elementsOutput += _elementStack.Pop();
                    if (_elementStack.Count > 0)
                    {
                        elementsOutput += ", ";
                    }
                }
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.ElementsNotClosed, elementsOutput));
            }

            _writer.Close();

            _stream.Seek(0, 0);

            StreamReader reader = new StreamReader(_stream);
            string output = reader.ReadToEnd();

            _stream.Close();
            _stream.Dispose();

            _flushed = true;

            return output;
        }
    }
}
